<!--
 * @Description: 文件上传组件二开
 * @Author: HuiSir<https://gitee.com/huisir001>
 * @Date: 2025-07-29 14:49:51
 * @LastEditTime: 2025-09-29 09:39:45
-->
<template>
  <!-- 文件上传 -->
  <div class="file-upload-box">
    <!-- 只暴露一个上传组件便于数据处理 - 隐藏 -->
    <el-upload
      ref="uploadRef"
      class="fy-form-upload"
      :file-list="uploadFilesList"
      :show-file-list="false"
      :on-change="handleFileChange"
      :on-success="handleUploadSuccess"
      :on-error="handleUploadError"
      :before-upload="handleBeforeUpload"
      :on-exceed="handlePicExceed"
      :auto-upload="autoUpload"
      :accept="accept"
      :action="action"
      :headers="headers"
      :name="name"
      :limit="limit"
      :data="data"
      :multiple="multiple"
    >
      <el-button type="primary">默认上传按钮（隐藏）</el-button>
    </el-upload>

    <div
      v-if="listType === 'picture-card'"
      class="el-upload-list el-upload-list--picture-card"
    >
      <div
        v-for="(url, ind) in uploadUrlList"
        :key="ind"
        class="el-upload-list__item is-success"
      >
        <el-image
          class="el-upload-list__item-thumbnail"
          :src="url"
          show-progress
          fit="cover"
        >
          <template #placeholder>
            <div class="upload-img-loading">
              <fy-icon
                name="Loading"
                class="is-loading"
                color="var(--el-color-primary)"
              />
              <span>加载中</span>
            </div>
          </template>
        </el-image>
        <div class="mask">
          <fy-icon
            name="ZoomIn"
            color="#fff"
            width="32px"
            height="32px"
            @click="handlePicPreview(ind)"
          ></fy-icon>
          <fy-icon
            v-if="!readonly"
            name="Delete"
            color="#fff"
            width="32px"
            height="32px"
            @click="handleUploadRemove(ind)"
          ></fy-icon>
        </div>
      </div>
      <div v-if="showUploadBtn" class="upload-card-btn">
        <!-- 带url插入的按钮 -->
        <el-popover
          v-if="allowUrlInsert"
          v-model:visible="popoverVisible"
          placement="bottom"
          :width="170"
          trigger="click"
        >
          <p class="popover-p">选择新增文件方式</p>
          <div class="popover-btns">
            <el-button
              type="primary"
              link
              @click="
                popoverVisible = false;
                showUrlInsertDialog = true;
              "
            >
              URL插入
            </el-button>
            <el-button
              type="primary"
              size="small"
              @click="
                fileSelect();
                popoverVisible = false;
              "
              >{{ autoUpload ? "立即上传" : "文件选择" }}
            </el-button>
          </div>
          <template #reference>
            <div class="el-upload el-upload--picture-card">
              <fy-icon name="Plus" size="28" color="inherit"></fy-icon>
            </div>
          </template>
        </el-popover>
        <div
          v-else
          class="el-upload el-upload--picture-card"
          @click="fileSelect"
        >
          <fy-icon name="Plus" size="28" color="inherit"></fy-icon>
        </div>
      </div>
    </div>
    <template v-else>
      <el-button-group v-show="showTextUploadBtn">
        <el-button
          :disabled="readonly"
          type="primary"
          size="small"
          @click="fileSelect"
          >{{ autoUpload ? "立即上传" : "文件选择" }}</el-button
        >
        <el-button
          v-if="allowUrlInsert"
          type="primary"
          size="small"
          @click.stop="showUrlInsertDialog = true"
          >URL插入</el-button
        >
      </el-button-group>
      <ul
        v-if="uploadFilesList.length"
        class="el-upload-list el-upload-list--text"
        :class="{ 'no-btn': !showTextUploadBtn }"
      >
        <li
          v-for="(item, ind) in uploadFilesList"
          :key="ind"
          class="el-upload-list__item is-success"
          tabindex="0"
        >
          <div class="el-upload-list__item-info">
            <a
              class="el-upload-list__item-name"
              :href="item.url"
              :download="item.name"
              target="_blank"
            >
              <fy-icon name="document" size="14" />
              <span class="el-upload-list__item-file-name" :title="item.name">{{
                item.name
              }}</span></a
            >
          </div>
          <label
            class="my-form-upload-list-label icon-delete"
            @click.stop="handleUploadRemove(ind)"
          >
            <fy-icon
              :name="item.status === 'success' ? 'circle-check' : 'Warning'"
              class="icon-circle-status"
              size="14"
              :height="28"
              :width="32"
              :color="
                item.status === 'success'
                  ? 'var(--el-color-success)'
                  : 'var(--el-color-warning)'
              "
            />
            <fy-icon
              v-if="!readonly"
              name="close"
              class="icon-close"
              size="14"
              :height="28"
              :width="32"
            />
          </label>
        </li>
      </ul>
    </template>
    <el-image-viewer
      v-if="showImgViewer"
      :url-list="imgViewUrlList"
      :initial-index="imgViewInitialIndex"
      :infinite="false"
      @close="showImgViewer = false"
    />
    <!-- URL插入 -->
    <urlInsertDialog
      v-model="showUrlInsertDialog"
      @submit="bindUrlInsertSubmit"
    />
  </div>
</template>

<script lang="ts" setup>
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ref, computed, watch, PropType, nextTick } from "vue";
import {
  UploadFile,
  UploadFiles,
  ElMessage,
  ElMessageBox,
  UploadRawFile,
  UploadInstance,
  UploadUserFile,
} from "element-plus";
import {
  getUrlFileName,
  getUrlFileExt,
  fileSizeCompute,
  showLoading,
} from "../../helpers";
import { MIME_TYPE } from "../../helpers/constant";
import { Awaitable, UploadEventParams } from "../types";
import { usePromiseSubmit } from "./usePromiseSubmit";
import urlInsertDialog from "./urlInsertDialog.vue";

// 组件名称（必要）
defineOptions({ name: "FyUpload" });

const props = defineProps({
  /**
   * 表单项key，在使用表单时候传入
   */
  formItemKey: {
    type: String,
    default: "",
  },
  /**
   * 类型，这里只有text和picture-card两种模式，弃用picture-list模式
   */
  listType: {
    type: String as PropType<"text" | "picture-card">,
    default: "text",
  },
  /**
   * 文件上传类型，默认array
   */
  fieldType: {
    type: String as PropType<"string" | "array">,
    default: "array",
  },
  /**
   * 允许上传的文件类型mimeType，如 image/png, image/jpeg, image/gif, image/svg+xml,application/pdf"
   */
  accept: {
    type: String,
  },
  /**
   * 上传接口路径
   */
  action: {
    type: String,
  },
  /**
   * 上传请求头
   */
  headers: {
    type: Object as PropType<Record<string, any>>,
  },
  /**
   * 上传的文件字段名 默认file
   */
  name: {
    type: String,
    default: "file",
  },
  /**
   * 限制上传数量,默认不限制
   */
  limit: {
    type: Number,
  },
  /**
   * 文件最大尺寸，单位：字节, 不传不限制
   */
  maxFileSize: {
    type: Number,
  },
  /**
   * 上传请求额外参数
   */
  data: {
    type: Object as PropType<
      | Record<string, any>
      | Awaitable<Record<string, any>>
      | ((rawFile: UploadRawFile) => Awaitable<Record<string, any>>)
    >,
  },
  /**
   * 回调（参数是请求回的内容），需要return上传后的文件地址，这里只支持单文件上传，所以返回的是单个文件的url
   */
  callback: {
    type: Function as PropType<(res: any) => string | void | null | boolean>,
  },
  /**
   * 是否显示按钮，当listType类型为text且上车数量等于limit数量时
   * 默认显示
   */
  showTextBtnIfFilesMoreThanLimit: {
    type: Boolean,
    default: true,
  },
  /**
   * 是否只读
   * 默认否
   */
  readonly: {
    type: Boolean,
    default: false,
  },
  /**
   * 是否自动执行上传文件请求，默认选择文件后自动请求
   * 这里与element-plus的autoUpload一致
   */
  autoUpload: {
    type: Boolean,
    default: true,
  },
  /**
   * 是否多选，默认否
   */
  multiple: {
    type: Boolean,
    default: false,
  },
  /**
   * 是否允许使用URL插入，默认是
   */
  allowUrlInsert: {
    type: Boolean,
    default: true,
  },
});

// 上传文件ref
const uploadRef = ref<UploadInstance>();

// 考虑到多文件上传，后端可能使用逗号分隔字符串实现，也可能使用数组，这里需要格式化
// 这里默认数组
const filesModel = defineModel({
  type: [String, Array, null] as PropType<string | string[] | null>,
  required: true,
  default: () => [],
});

const emit = defineEmits<{
  /**
   * 文件change触发事件
   */
  "upload:change": [params: UploadEventParams];
}>();

// 文件上传loading
const uploadLoading = ref();

// 文件传入类型
const fieldType = computed(() => {
  const fileList = filesModel.value;
  if (typeof fileList === "string") {
    return "string";
  } else if (Array.isArray(fileList)) {
    return "array";
  } else {
    return props.fieldType;
  }
});

// 文件列表
const uploadFilesList = ref<UploadUserFile[]>([]);

// 转化
const uploadUrlList = computed(() =>
  uploadFilesList.value.map((item) => item.url!)
);

// 手动提交封装
const { onChange, promiseSubmit } = usePromiseSubmit(
  uploadRef,
  uploadFilesList
);

// 取原文件
const someUploadFile = (url: string) =>
  uploadFilesList.value.some((item) => item.url === url);

// 文件内容格式化
watch(
  filesModel,
  (val, oldVal) => {
    // 只监听初次
    if (oldVal && oldVal.length) {
      return;
    }

    const urls =
      typeof val === "string" && val.trim()
        ? val.split(",")
        : Array.isArray(val)
        ? val
        : [];

    // 若在文件列表中已存在，则不赋值
    if (urls.some((url) => someUploadFile(url))) {
      return;
    }

    uploadFilesList.value = urls.map((url, ind) => ({
      name: getUrlFileName(url),
      url,
      status: "success",
      uid: Date.now() + ind,
    }));
  },
  { immediate: true }
);

// 是否展示按钮（text类型时）
const showTextUploadBtn = computed(() => {
  if (props.limit == void 0) return true;
  return (
    (uploadFilesList.value.length >= props.limit &&
      props.showTextBtnIfFilesMoreThanLimit) ||
    uploadFilesList.value.length < props.limit
  );
});

// 缓存
const uploadTemp = ref<{ uploadFile?: UploadFile; uploadFiles?: UploadFiles }>(
  {}
);

// 文件选择
const fileSelect = () => {
  // 触发上传组件的文件选择
  const input = uploadRef.value?.$el.querySelector('input[type="file"]');
  if (input) {
    input.click();
  }
};

// 文件改变钩子
const handleFileChange = (uploadFile: UploadFile, uploadFiles: UploadFiles) => {
  // 缓存
  uploadTemp.value.uploadFile = uploadFile;
  uploadTemp.value.uploadFiles = uploadFiles;

  // 手动上传时保存
  if (!props.autoUpload && uploadFile.raw) {
    // 手动上传时文件验证
    if (!fileValidate(uploadFile.raw)) return;

    const index = uploadFilesList.value.findIndex(
      (item) => item.uid === uploadFile.uid
    );

    if (
      uploadFile?.response &&
      uploadFile?.status === "success" &&
      index >= 0
    ) {
      if (!props.callback) {
        throw Error("缺少callback函数");
      }
      // 上传成功替换原有文件
      uploadFilesList.value[index].status = "success";
      const url = props.callback(uploadFile.response);
      if (!url) {
        uploadFile.status = "fail";
        uploadFilesList.value.splice(index, 1);
        // 移除缓存
        removeFileFormCache(uploadFile.uid!);
      } else {
        uploadFilesList.value[index].url = url as string;
      }
    } else if (uploadFile?.status === "fail" && index >= 0) {
      // 上传失败删除原有文件
      uploadFilesList.value.splice(index, 1);
      // 移除缓存
      removeFileFormCache(uploadFile.uid!);
    } else {
      uploadFilesList.value.push({
        url: URL.createObjectURL(uploadFile.raw),
        name: uploadFile.name,
        uid: uploadFile.uid,
        status: uploadFile.status,
      });
    }

    // 触发状态
    onChange(uploadFile);
  }

  // 文件change触发事件
  emit("upload:change", {
    uploadFile,
    uploadFiles,
    formItemKey: props.formItemKey,
  });
};

/**
 * 文件验证
 */
const fileValidate = (file: UploadRawFile) => {
  if (props.accept && !props.accept.includes(file.type)) {
    ElMessage.error(`文件扩展类型有误！允许的文件类型为：${props.accept}`);
    removeInvalidFile(file);
    return false;
  }

  if (props.maxFileSize && file.size && file.size > props.maxFileSize) {
    // 文件大小计算
    ElMessage.error(`文件不允许超过 ${fileSizeCompute(props.maxFileSize)}！`);
    removeInvalidFile(file);
    return false;
  }

  return true;
};

// 文件上传前钩子，验证文件
const handleBeforeUpload = (file: UploadRawFile) => {
  // 这里只有自动上传时验证，手动上传已经在change事件中验证了，这里不重复验证
  if (props.autoUpload && !fileValidate(file)) {
    return false;
  }

  uploadLoading.value = showLoading("上传中");

  return true;
};

// 手动从文件列表中删除非法文件
const removeInvalidFile = async (invalidFile: UploadRawFile) => {
  await nextTick();
  uploadFilesList.value = uploadFilesList.value.filter(
    (file) => file.uid !== invalidFile.uid // 通过 uid 匹配删除
  );
  // 缓存移除
  removeFileFormCache(invalidFile.uid);
};

// 移除缓存中的文件
const removeFileFormCache = (uid: number) => {
  const tempIndex = uploadTemp.value.uploadFiles?.findIndex(
    (item) => item.uid === uid
  );

  if (tempIndex != void 0 && tempIndex >= 0) {
    uploadTemp.value.uploadFiles?.splice(tempIndex, 1);
  }
};

// 上传失败钩子
const handleUploadError = (error: Error) => {
  if (props.autoUpload) {
    uploadRef.value?.clearFiles(); // 清理组件内部状态
    uploadLoading.value.close();
    console.error("上传文件失败：", error);
  }
};

// 文件上传成功钩子
const handleUploadSuccess = (
  res: Record<string, unknown>,
  uploadFile: UploadFile
) => {
  // 这里只处理自动上传逻辑
  if (!props.autoUpload) return;

  if (!props.callback) throw Error("缺少callback函数");

  // 这里只支持单个文件上传，所以返回的是单个文件url
  const fileUrl = props.callback(res);

  if (!fileUrl) {
    ElMessage.error("上传失败！");
    console.error("上传失败：", res);
    // 关闭loading
    uploadLoading.value.close();
    uploadRef.value?.clearFiles(); // 清理组件内部状态
    return;
  }

  // 更新父数据
  const uploadList = [...uploadUrlList.value, fileUrl as string];
  filesModel.value =
    fieldType.value === "array" ? uploadList : uploadList.join(",");

  // 视图更新
  uploadFilesList.value.push({
    url: fileUrl as string,
    name: getUrlFileName(fileUrl as string),
    uid: uploadFile.uid,
    status: uploadFile.status,
  });

  console.log("上传成功：", res);
  // 关闭loading
  uploadLoading.value.close();
};

// 文件移除钩子
const handleUploadRemove = (index: number = 0) => {
  ElMessageBox.confirm("确定要移除该文件吗？", {
    title: "提示",
  })
    .then(() => {
      if (index >= 0) {
        // 清除input的值，以便可以重新选择相同的文件
        const input = uploadRef.value?.$el.querySelector('input[type="file"]');
        if (input) {
          input.value = "";
        }

        const uid = uploadFilesList.value[index].uid;

        uploadFilesList.value.splice(index, 1);

        // 移除缓存
        removeFileFormCache(uid!);
      }

      // 自动上传时更新源数据
      if (props.autoUpload) {
        filesModel.value =
          fieldType.value === "array"
            ? uploadUrlList.value
            : uploadUrlList.value.join(",");
      }

      // 文件change触发事件
      emit("upload:change", {
        uploadFiles: uploadTemp.value.uploadFiles!,
        formItemKey: props.formItemKey,
      });
    })
    .catch(() => {
      // catch error
    });
};

/**
 * 超出数量限制时执行钩子
 */
const handlePicExceed = () => {
  if (props.limit && props.limit > 0) {
    ElMessage.error(`只允许上传${props.limit}个文件!`);
  }
};

// 是否展示上传按钮
const showUploadBtn = computed(() => {
  if (props.readonly) {
    return false;
  }
  if (props.limit) {
    return props.limit > uploadFilesList.value.length;
  }
  // 默认不限制
  return true;
});

// 图片预览
const imgViewUrlList = ref<string[]>([]);
const showImgViewer = ref(false);
const imgViewInitialIndex = ref(0);
const handlePicPreview = (index: number) => {
  imgViewUrlList.value = uploadUrlList.value;
  showImgViewer.value = true;
  imgViewInitialIndex.value = index;
};

// 禁止自动上传时可手动执行上传
const submit = async () => {
  if (!props.autoUpload && uploadRef.value) {
    let promiseSubmitRes: unknown = [];
    try {
      promiseSubmitRes = await promiseSubmit();
      // 全部上传完成后再更新源文件，避免操作filesModel影响组件内部逻辑导致BUG
      filesModel.value =
        fieldType.value === "array"
          ? uploadUrlList.value
          : uploadUrlList.value.join(",");
    } catch {
      console.log("文件上传失败");
    }
    // 关闭loading
    uploadLoading.value?.close();

    return promiseSubmitRes;
  }
};

// URL插入
const popoverVisible = ref(false);
const showUrlInsertDialog = ref(false);
const bindUrlInsertSubmit = (url: string) => {
  if (props.limit && props.limit === uploadFilesList.value.length) {
    ElMessage.error(`只允许上传${props.limit}个文件!`);
    return;
  }

  const uid = Date.now();
  const name = getUrlFileName(url);
  const extName = getUrlFileExt(url);
  const uploadFile: UploadFile = {
    url,
    uid,
    name,
    percentage: 100,
    status: "success",
    raw: {
      uid,
      name,
      lastModified: uid,
      type: extName ? MIME_TYPE[`.${extName}`] : "",
    } as UploadRawFile,
  };

  // 缓存
  uploadTemp.value.uploadFile = uploadFile;
  uploadTemp.value.uploadFiles?.push(uploadFile);

  // 手动上传时文件验证
  if (!fileValidate(uploadFile.raw!)) return false;

  // 插入文件列表
  uploadFilesList.value.push({
    url,
    name,
    uid,
    status: "success",
  });

  // 文件change触发事件
  emit("upload:change", {
    uploadFile,
    uploadFiles: uploadTemp.value.uploadFiles!,
    formItemKey: props.formItemKey,
  });

  showUrlInsertDialog.value = false;
};

defineExpose({
  submit,
  formItemKey: props.formItemKey,
  uploadUrlList,
});
</script>

<style scoped lang="scss">
.file-upload-box {
  width: 100%;
}
.fy-form-upload {
  display: none;
}
.upload-card-btn {
  margin-bottom: 8px;
}
.el-upload-list--picture-card {
  width: 100%;

  .el-upload-list__item {
    position: relative;
    margin: 0 8px 8px 0;
    &:hover .mask {
      opacity: 1;
    }
    .upload-img-loading {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      width: 100%;
      height: 100%;
      color: var(--el-color-primary);
    }
    .mask {
      display: flex;
      transition: opacity 0.3s;
      position: absolute;
      width: 100%;
      height: 100%;
      left: 0;
      top: 0;
      align-items: center;
      justify-content: center;
      background-color: rgba(0, 0, 0, 0.5);
      opacity: 0;
      .el-icon {
        cursor: pointer;
      }
      .el-icon + .el-icon {
        margin-left: 20px;
      }
    }
  }
}
:deep(.el-upload) {
  &.el-upload--text {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    margin-top: 4px;
  }
}
.el-upload-list.el-upload-list--text {
  margin: 4px 0 0;
  &.no-btn {
    margin-top: 0 !important;
  }
  .el-upload-list__item {
    background-color: var(--el-fill-color-light);
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    .el-upload-list__item-info {
      width: unset;
      max-width: calc(100% - 60px);
    }
    .my-form-upload-list-label {
      cursor: pointer;
      height: 28px;
      .icon-close {
        display: none;
      }
    }
    &:hover {
      .my-form-upload-list-label {
        .icon-close {
          display: inline-flex;
        }
        .icon-circle-status {
          display: none;
        }
      }
    }
  }
}
</style>
<style lang="scss">
.popover-p {
  text-align: center;
  margin-bottom: 8px;
}
.popover-btns {
  display: flex;
  align-items: center;
  justify-content: space-between;
  .el-upload.el-upload--text {
    margin-top: 0 !important;
  }
}
</style>
